package com.gitee.simpleplus.jproxy;

import static org.apache.commons.lang3.CharUtils.LF;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtNewMethod;

public class FactoryGenerator {

	private static final Logger log = LoggerFactory.getLogger(FactoryGenerator.class);
	private static final char LINE_BREAK;
	static {
		LINE_BREAK = log.isInfoEnabled() ? LF : ' ';
	}

	public static <T> Class<T> genFactory(Class<T> inf, Set<Class<?>> classList) {
		if (!inf.isInterface()) {
			throw new RuntimeException("inf:" + inf.getName() + " is not a interface");
		}
		Factory fa = inf.getAnnotation(Factory.class);
		if (fa == null) {
			throw new RuntimeException("inf:" + inf.getName() + " donnt exist Factory Annotation");
		}
		return genFactory(inf, fa,
				classList.stream().filter(cls -> cls.isAnnotationPresent(fa.value())).collect(Collectors.toList()));
	}

	private static <T> Class<T> genFactory(Class<T> inf, Factory fa, List<Class<?>> collect) {

		Method m = null;
		Method[] declaredMethods = inf.getDeclaredMethods();

		for (Method method : declaredMethods) {
			if (!method.isDefault()) {
				if (m == null) {
					m = method;
				} else {
					throw new RuntimeException("only allow one need factory method in interface!," + inf.getName());
				}
			}
		}

		if (m == null) {
			throw new RuntimeException("need one factory method in interface!," + inf.getName());
		}

		int typesLen = m.getParameterCount();

		Class<?>[] types = m.getParameterTypes();
		Class<?>[] typesConstructor = new Class<?>[types.length - 1];
		System.arraycopy(types, 1, typesConstructor, 0, typesConstructor.length);

		try {
			ClassPool cp = ClassPool.getDefault();
			CtClass ctClazzInf = cp.get(inf.getName());

			CtClass ctClazzChild = cp.makeClass(inf.getName() + "$$Proxy");

			ctClazzChild.addInterface(ctClazzInf);

			Class<? extends Annotation> value = fa.value();

			Method method = value.getDeclaredMethods()[0];

			StringBuilder body = new StringBuilder();

			body.append("switch($1){").append(LINE_BREAK);
			collect.forEach(c -> {
				try {
					c.getConstructor(typesConstructor);// check if exist construct

					Object invoke = method.invoke(c.getAnnotation(value));
					if (invoke.getClass().isArray()) {
						for (int i = 0; i < Array.getLength(invoke); i++) {
							appendCaseStatement(typesLen, body, c, Array.get(invoke, i));
						}
					} else {
						appendCaseStatement(typesLen, body, c, invoke);
					}
				} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
						| NoSuchMethodException | SecurityException e) {
					throw new RuntimeException(e);
				}

			});
			body.append("default: return null;").append(LINE_BREAK).append("}");
			addCtMethod(ctClazzChild, m, cp, body);
			log.info("inf:{}, methodName:{}, body:{}", inf.getName(), m.getName(), body);
			return ctClazzChild.toClass();
		} catch (Exception e1) {
			throw new RuntimeException(e1);
		}
	}

	private static void appendCaseStatement(int typesLen, StringBuilder body, Class<?> c, Object val) {
		body.append("case ").append(val).append(": return new ").append(c.getName()).append("(");

		for (int j = 0; j < typesLen - 1; j++) {
			body.append(j == 0 ? "" : ",").append("$").append(j + 2);
		}

		body.append(");").append(LINE_BREAK);
	}

	private static CtClass[] covertClazz(ClassPool cp, Class<?>[] clazzes) throws Exception {
		CtClass[] ctClazz = new CtClass[clazzes.length];
		for (int i = 0; i < clazzes.length; i++) {
			ctClazz[i] = cp.get(clazzes[i].getName());
		}
		return ctClazz;
	}

	private static void addCtMethod(CtClass ctClazzChild, Method m, ClassPool cp, StringBuilder body) throws Exception {
		ctClazzChild.addMethod(CtNewMethod.make(cp.get(m.getReturnType().getName()), m.getName(),
				covertClazz(cp, m.getParameterTypes()), covertClazz(cp, m.getExceptionTypes()), body.toString(),
				ctClazzChild));

	}
}
